﻿using System;
using System.Data;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Data.SqlClient;
using System.Linq;

using BizElements.Core.ProviderComponents;

namespace BizElements.Core.SqlServer
{
    internal sealed class SqlServerBulkSaver : IBulkSaver
    {
        #region Helper methods.

        private static IEntityDAO GetDao(IConnectionProvider conn, IEntity entity)
        {
            return EntityDaoFactory.GetEntityDAO(entity, conn);
        }

        #endregion

        #region BulkSave - splits collection into 3 parts (new, changed and removed entities) and executes bulk operations on each part.

        /// <summary>Saves entity collection.</summary>
        /// <param name="conn">Connection-transaction context.</param>
        /// <param name="table">Target database table.</param>
        /// <param name="entitiesToSave">A collection of entities.</param>
        public void BulkSave(IConnectionProvider conn, IDbTable table, IEnumerable entitiesToSave)
        {
            List<IEntity> newEntities, changedEntities, removedEntities;
            SplitCollectionForInsertUpdateAndDeleteOperations(entitiesToSave, out newEntities, out changedEntities, out removedEntities);

            bool connIsLocal = !conn.IsOpen;
            if (connIsLocal)
                conn.OpenConnection();

            try
            {
                BulkInsert(conn, table, newEntities);
                BulkUpdate(conn, table, changedEntities);
                BulkDelete(conn, table, removedEntities);
            }
            finally
            {
                if (connIsLocal)
                    conn.CloseConnection();
            }
        }

        private static void SplitCollectionForInsertUpdateAndDeleteOperations(IEnumerable entitiesToSave, out List<IEntity> newEntities, out List<IEntity> changedEntities, out List<IEntity> removedEntities)
        {
            newEntities = new List<IEntity>();
            changedEntities = new List<IEntity>();
            removedEntities = new List<IEntity>();
            foreach (IEntity entity in entitiesToSave)
            {
                switch (entity.EntityState)
                {
                    case EntityState.New:
                        newEntities.Add(entity);
                        break;
                    case EntityState.OutOfSync:
                        changedEntities.Add(entity);
                        break;
                    case EntityState.PendingDeletion:
                        removedEntities.Add(entity);
                        break;
                }
            }
        }

        #endregion

        #region BulkInsert - optimized using BCP.

        /// <summary>Insert entities.</summary>
        /// <param name="conn">Connection-transaction context.</param>
        /// <param name="table">Target database table.</param>
        /// <param name="newEntities">A collection of entities.</param>
        public void BulkInsert(IConnectionProvider conn, IDbTable table, IEnumerable newEntities)
        {
            if (ArrayUtil.IsNullOrEmpty(newEntities))
                return;

            DataTable newRecords = newEntities.ToDataTable(table);
            var bulkCopy = BuildSqlBulkCopy(conn, table.TableName, newRecords);
            bulkCopy.WriteToServer(newRecords);
        }

        private static SqlBulkCopy BuildSqlBulkCopy(IConnectionProvider conn, string destinationTableName, DataTable newRecords)
        {            
            SqlBulkCopy bulkCopy = new SqlBulkCopy(GetSqlConnection(conn), GetSqlBulkCopyOptions(), GetSqlTransaction(conn));
            bulkCopy.DestinationTableName = destinationTableName;
            // AutoIdentity columns may be mapped, but SQL Server will ignore them.
            foreach (DataColumn col in newRecords.Columns)
                bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);

            return bulkCopy;
        }

        private static SqlConnection GetSqlConnection(IConnectionProvider conn)
        {
            return (SqlConnection)conn.Connection;
        }

        private static SqlTransaction GetSqlTransaction(IConnectionProvider conn)
        {
            return (conn.IsTransactionPending) ? (SqlTransaction)conn.Transaction : null;
        }

        const SqlBulkCopyOptions CheckConstraintsAndFireTriggers = SqlBulkCopyOptions.CheckConstraints | SqlBulkCopyOptions.FireTriggers;

        private static SqlBulkCopyOptions GetSqlBulkCopyOptions()
        {
            // In future, custom options may be allowed.
            return CheckConstraintsAndFireTriggers;
        }

        #endregion

        #region BulkUpdate - not optimized.

        /// <summary>Updates entities.</summary>
        /// <param name="conn">Connection-transaction context.</param>
        /// <param name="table">Target database table.</param>
        /// <param name="changedEntities">A collection of entities.</param>
        public void BulkUpdate(IConnectionProvider conn, IDbTable table, IEnumerable changedEntities)
        {
            if (ArrayUtil.IsNullOrEmpty(changedEntities))
                return;

            bool connIsLocal = !conn.IsOpen;
            if (connIsLocal)
                conn.OpenConnection();

            try
            {
                foreach (IEntity entity in changedEntities)
                    GetDao(conn, entity).UpdateOne();
            }
            finally
            {
                if (connIsLocal)
                    conn.CloseConnection();
            }
        }

        #endregion

        #region BulkDelete - not optimized.

        /// <summary>Deletes entities.</summary>
        /// <param name="conn">Connection-transaction context.</param>
        /// <param name="table">Target database table.</param>
        /// <param name="removedEntities">A collection of entities.</param>
        public void BulkDelete(IConnectionProvider conn, IDbTable table, IEnumerable removedEntities)
        {
            if (ArrayUtil.IsNullOrEmpty(removedEntities))
                return;

            bool connIsLocal = !conn.IsOpen;
            if (connIsLocal)
                conn.OpenConnection();

            try
            {
                foreach (IEntity entity in removedEntities)
                    GetDao(conn, entity).DeleteOne();
            }
            finally
            {
                if (connIsLocal)
                    conn.CloseConnection();
            }
        }

        #endregion
    }
}